/**
 * 公用模块
 *
 * Created by snowingsea on 2020/02/22.
 */
const EventEmitter = require('events');
const mongoose = require('mongoose');
const Func = require('./mongo/function');
const File = require('./mongo/file');
const ProjectVersion = require('./mongo/project-version');
const Project = require('./mongo/project');
const User = require('./mongo/user');
const publisher = require('./redis/publisher');
const utility = require('./utility');
const fileElement = require('./file-elements');
const functions = require('./functions');

/**
 * 补全用户信息
 * @param {String} userId 用户id
 * @param {User} user 用户
 * @return {Promise}
 */
function complementByUserId(userId, user = null) {
  return new Promise((resolve, reject) => {
    Promise.resolve()
      .then(() => {
        if (user) {
          return Promise.resolve(user);
        }
        return User.findOne({ _id: mongoose.Types.ObjectId(userId) });
      })
      .then((user) => {
        resolve({ user });
      })
      .catch(reject);
  });
}

/**
 * 补全项目、创建者
 * @param {String} projectId 项目id
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function complementByProjectId(projectId, project = null, creator = null) {
  return new Promise((resolve, reject) => {
    Promise.resolve()
      .then(() => {
        if (project) {
          return Promise.resolve(project);
        }
        return Project.findOne({ _id: mongoose.Types.ObjectId(projectId) });
      })
      .then((res) => {
        project = res;
        return complementByUserId(project.creatorId, creator);
      })
      .then(({ user }) => {
        resolve({ project, creator: user });
      })
      .catch(reject);
  });
}

/**
 * 补全项目版本、项目、创建者
 * @param {String} versionId 版本id
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function complementByProjectVersionId(versionId, version = null, project = null, creator = null) {
  return new Promise((resolve, reject) => {
    Promise.resolve()
      .then(() => {
        if (version) {
          return Promise.resolve(version);
        }
        return ProjectVersion.findOne({ _id: mongoose.Types.ObjectId(versionId) });
      })
      .then((res) => {
        version = res;
        return complementByProjectId(version.projectId, project, creator);
      })
      .then(({ project, creator }) => {
        resolve({ version, project, creator });
      })
      .catch(reject);
  });
}

/**
 * 补全文件、项目版本、项目、创建者
 * @param {String} fileId 文件id
 * @param {File} file 文件
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function complementByFileId(fileId, file = null, version = null, project = null, creator = null) {
  return new Promise((resolve, reject) => {
    Promise.resolve()
      .then(() => {
        if (file) {
          return Promise.resolve(file);
        }
        return File.findOne({ _id: mongoose.Types.ObjectId(fileId) });
      })
      .then((res) => {
        file = res;
        return complementByProjectVersionId(file.versionId, version, project, creator);
      })
      .then(({ version, project, creator }) => {
        resolve({
          file,
          version,
          project,
          creator,
        });
      })
      .catch(reject);
  });
}

/**
 * 重命名mongo键值，去除"."
 * @param {String} key 键值
 * @return {String}
 */
function renameMongoKey(key) {
  if (!key) {
    return '_';
  }
  return utility.replaceAll(key, '.', '_');
}

/**
 * 获取文件内的统计信息
 * @param {Object} file 文件信息
 * @param {Array} extensions 后缀名列表
 * @returns {Object}
 */
function getCountOfFile(file, extensions = undefined) {
  const extSet = new Set();
  if (extensions) {
    if (!utility.isArrayType(extensions)) {
      extensions = [extensions];
    }
    extensions.forEach((ext) => {
      extSet.add(renameMongoKey(ext));
    });
  }
  const count = {
    failedCount: 0,
    succeedCount: 0,
    statementCovered: 0,
    statementTotal: 0,
    branchCovered: 0,
    branchTotal: 0,
    conditionCovered: 0,
    conditionTotal: 0,
    decisionCovered: 0,
    decisionTotal: 0,
    mcdcCovered: 0,
    mcdcTotal: 0,
    invokedFuncTotal: 0,
    invokedFuncCovered: 0,
    invokeTotal: 0,
    invokeCovered: 0,
    fileCount: 0,
    compiledCount: 0,
    compileFailedCount: 0,
    unreadCount: 0,
    waiting4compilingCount: 0,
  };
  Object.keys(count).forEach((key) => {
    if (!file[key]) {
      return;
    }
    Object.keys(file[key]).forEach((ext) => {
      if (!extensions || extSet.has(ext)) {
        count[key] += file[key][ext];
      }
    });
  });
  return count;
}

/**
 * 发布根目录更新消息
 * @param {Object} data 消息内容
 * @return {Promise}
 */
function publishRootDirectoryUpdateMessage(data) {
  return new Promise((resolve, reject) => {
    ProjectVersion.findOne({ _id: mongoose.Types.ObjectId(data.versionId) })
      .then((projectVersion) => {
        let suffixes = [];
        Object.keys(projectVersion.languages).forEach((key) => {
          suffixes = suffixes.concat(projectVersion.languages[key].suffixes);
        });
        const extensions = suffixes.concat(projectVersion.headers.suffixes);
        const count = getCountOfFile(data, extensions);
        if (count.statementTotal) {
          data.statementCoverage = count.statementCovered / count.statementTotal;
        }
        if (count.branchTotal) {
          data.branchCoverage = count.branchCovered / count.branchTotal;
        }
        if (count.conditionTotal) {
          data.conditionCoverage = count.conditionCovered / count.conditionTotal;
        }
        if (count.decisionTotal) {
          data.decisionCoverage = count.decisionCovered / count.decisionTotal;
        }
        if (count.mcdcTotal) {
          data.mcdcCoverage = count.mcdcCovered / count.mcdcTotal;
        }
        if (count.invokedFuncTotal) {
          data.invokedFuncCoverage = count.invokedFuncCovered / count.invokedFuncTotal;
        }
        if (count.invokeTotal) {
          data.invokeCoverage = count.invokeCovered / count.invokeTotal;
        }
        if (count.fileCount) {
          if (count.failedCount > 0 || count.succeedCount > 0) {
            data.failed = count.failedCount > 0;
          }
        }
        publisher.publishRootDirectoryMessage('update', data);
        resolve();
      })
      .catch(reject);
  });
}

function increaseActionMerger(action) {
  const map = new Map();
  let triggered = 0;
  const event = new EventEmitter();
  event.setMaxListeners(1000);
  return function fn(docId, incDoc, ...args) {
    const str = Array.isArray(docId)
      ? docId.map((did) => did.toString()).join(',')
      : docId.toString();
    if (!str) {
      return Promise.resolve();
    }
    // console.log('merge:', action.name, str, incDoc);
    let oldDoc = map.get(str);
    if (!oldDoc) {
      oldDoc = { $inc: {}, __id: docId };
    }
    // 合并累加
    Object.keys(incDoc.$inc).forEach((key) => {
      const value = incDoc.$inc[key];
      const oldValue = oldDoc.$inc[key] || 0;
      oldDoc.$inc[key] = oldValue + value;
    });
    map.set(str, oldDoc);
    const ps = new Promise((res, rej) => {
      event.once(str, (err, data) => {
        if (err) {
          return rej(err);
        }
        return res(data);
      });
    });

    if (!triggered) {
      triggered = 1;
      setImmediate(() => {
        triggered = 0;
        map.forEach((incDoc, k) => {
          map.delete(k);
          const { __id } = incDoc;
          delete incDoc.__id;
          // console.log('do action:', action.name, k, incDoc);
          action(__id, incDoc, ...args)
            .then((data) => {
              event.emit(k, undefined, data);
            })
            .catch((err) => {
              event.emit(k, err, undefined);
            });
        });
      });
    }

    return ps;
  };
}

/**
 * 更新目录增量数据
 * @param {String} directoryId 目录id
 * @param {Object} incDoc 增量数据
 * @param {Object} file 文件version以及path
 * @return {Promise}
 */
async function increaseDirectories(directoryId, incDoc, {
  versionId,
  path,
} = {}) {
  if (!directoryId) {
    return;
  }
  if (!versionId || !path) {
    const dir = await File.findOne({ _id: directoryId });
    versionId = dir.versionId;
    path = dir.path;
  }
  const pathArr = path.split('/').reduce((arr, item) => arr.concat(arr.length ? `${arr[arr.length - 1]}/${item}` : item), []);
  await File.updateMany({
    versionId,
    path: { $in: pathArr },
  }, incDoc);
  // const keys = Object.keys(incDoc.$inc);
  // const pipline = [
  //   {
  //     $set: keys.reduce((obj, k) => {
  //       if (incDoc.$inc[k]) {
  //         obj[k] = { $add: [{ $ifNull: [`$${k}`, 0] }, incDoc.$inc[k]] };
  //       }
  //       return obj;
  //     }, {}),
  //   },
  //   // unset值为0的字段
  //   {
  //     $set: keys.reduce((obj, k) => {
  //       if (incDoc.$inc[k]) {
  //         obj[k] = {
  //           $cond: { if: { $eq: [`$${k}`, 0] }, then: '$$REMOVE', else: `$${k}` },
  //         };
  //       }
  //       return obj;
  //     }, {}),
  //   },
  // ];
  // console.log(JSON.stringify(pipline, null, 1));
  // await File.updateMany({
  //   versionId,
  //   path: { $in: pathArr },
  // }, pipline);

  // redis publish message
  (async () => {
    const files = await File.find({
      versionId,
      path: { $in: pathArr },
    });
    files.forEach((file) => {
      const data = {
        fileId: file._id,
        creatorId: file.creatorId,
        projectId: file.projectId,
        versionId: file.versionId,
      };
      Object.keys(incDoc.$inc).forEach((key) => {
        const keyName = key.split('.')[0];
        data[keyName] = file[keyName];
        if (keyName.endsWith('Covered')) {
          const totalKeyName = keyName.replace('Covered', 'Total');
          data[totalKeyName] = file[totalKeyName];
        }
        if (keyName.endsWith('Total')) {
          const coveredKeyName = keyName.replace('Total', 'Covered');
          data[coveredKeyName] = file[coveredKeyName] || 0;
        }
        switch (keyName) {
          case 'compileFailedCount':
          case 'compiledCount':
          case 'shieldFilesCount':
          case 'waiting4compilingCount':
            data.fileCount = file.fileCount;
            break;
          default:
            break;
        }
      });
      if (file.directoryId) {
        publisher.publishFileMessage('update', data);
      } else {
        data.fileCount = file.fileCount;
        publishRootDirectoryUpdateMessage(data).then(() => {}).catch(() => {});
      }
    });
  })();
}

const increaseDirectoriesMerged = increaseActionMerger(increaseDirectories);

// eslint-disable-next-line no-unused-vars
function increaseDirectoriesMerge() {
  const map = new Map();
  let triggered = 0;
  let stableLoop = 0;
  return function fn(directoryId, incDoc, cb) {
    const oldDoc = map.get(directoryId) || { $inc: {}, cbs: [] };
    // 合并累加
    Object.keys(incDoc.$inc).forEach((key) => {
      const value = incDoc.$inc[key];
      const oldValue = oldDoc.$inc[key] || 0;
      oldDoc.$inc[key] = oldValue + value;
    });
    oldDoc.cbs.push(cb);
    map.set(directoryId, oldDoc);
    stableLoop = 0;
    if (triggered) {
      return;
    }
    triggered = 1;
    (function loop() {
      stableLoop += 1;
      if (stableLoop < 3) {
        setImmediate(loop);
        return;
      }
      triggered = 0;
      map.forEach((incDoc, directoryId) => {
        map.delete(directoryId);
        const { cbs } = incDoc;
        delete incDoc.cbs;
        increaseDirectories(directoryId, incDoc)
          .then(() => {
            cbs.forEach((cb) => cb());
          })
          .catch((err) => {
            cbs.forEach((cb) => cb(err));
          });
      });
    }());
  };
}

/**
 * 更新文件增量数据
 * @param {String} fileId 文件id
 * @param {Object} incDoc 增量数据
 * @return {Promise}
 */
function increaseFile(fileId, incDoc, {
  batch = false,
} = {}) {
  if (!fileId) {
    return Promise.resolve();
  }
  fileId = mongoose.Types.ObjectId(fileId);
  return new Promise((resolve, reject) => {
    File.findOne({ _id: fileId })
      .then((file) => {
        if (!file) {
          return Promise.resolve();
        }
        let { extname } = file;
        extname = renameMongoKey(extname);
        const { directoryId } = file;
        const doc = { $inc: {} };
        Object.keys(incDoc.$inc).forEach((key) => {
          doc.$inc[`${key}.${extname}`] = incDoc.$inc[key];
        });
        const promises = [
          File.updateOne({ _id: fileId }, doc),
          batch ? increaseDirectoriesMerged(directoryId, doc) : increaseDirectories(directoryId, doc),
        ];
        return Promise.all(promises);
      })
      .then((res) => {
        if (!res) {
          return Promise.resolve();
        }
        return Promise.all([
          File.findOne({ _id: fileId }),
          functions.getDynamicStatistics(null, fileId),
        ]);
      })
      .then(([
        file,
        {
          testsCount,
          funcsCount,
          testedFuncsCount,
          testedTestsCount,
        },
      ]) => {
        if (!file) {
          return Promise.resolve();
        }
        const data = {
          fileId,
          creatorId: file.creatorId,
          projectId: file.projectId,
          versionId: file.versionId,
          funcsCount,
          testsCount,
          testedFuncsCount,
          testedTestsCount,
        };
        Object.keys(incDoc.$inc).forEach((key) => {
          data[key] = file[key];
          if (key.endsWith('Covered')) {
            const totalKey = key.replace('Covered', 'Total');
            data[totalKey] = file[totalKey] || 0;
          }
          if (key.endsWith('Total')) {
            const coveredKey = key.replace('Total', 'Covered');
            data[coveredKey] = file[coveredKey] || 0;
          }
          switch (key) {
            case 'compileFailedCount':
            case 'compiledCount':
            case 'waiting4compilingCount':
              data.fileCount = file.fileCount;
              break;
            default:
              break;
          }
        });
        publisher.publishFileMessage('update', data);
        return file;
      })
      .then((res) => resolve(res))
      .catch(reject);
  });
}

const increaseFileMerged = increaseActionMerger(increaseFile);

/**
 * 更新函数增量数据，一次查询，不用递归处理
 * @param {Array<ObjectId>} parentIds 函数ids
 * @param {Object} incDoc 增量数据
 * @return {Promise}
 */
function increaseFunctionParents(parentIds, incDoc) {
  if (!parentIds.length) {
    return Promise.resolve();
  }
  return new Promise((resolve, reject) => {
    Func.updateMany({ _id: { $in: parentIds } }, incDoc)
      .then(() => Func.find({ _id: { $in: parentIds } }))
      .then((functions) => {
        functions.forEach((func) => {
          const functionData = {
            // eslint-disable-next-line no-underscore-dangle
            functionId: func._id,
            versionId: func.versionId,
            creatorId: func.creatorId,
            fileId: func.fileId,
            statementCoverage: null,
            branchCoverage: null,
            conditionCoverage: null,
            decisionCoverage: null,
            mcdcCoverage: null,
            invokeCoverage: null,
            invokedFuncCoverage: null,
          };
          if (func.statementTotal) {
            functionData.statementCoverage = func.statementCovered / func.statementTotal;
          }
          if (func.branchTotal) {
            functionData.branchCoverage = func.branchCovered / func.branchTotal;
          }
          if (func.conditionTotal) {
            functionData.conditionCoverage = func.conditionCovered / func.conditionTotal;
          }
          if (func.decisionTotal) {
            functionData.decisionCoverage = func.decisionCovered / func.decisionTotal;
          }
          if (func.mcdcTotal) {
            functionData.mcdcCoverage = func.mcdcCovered / func.mcdcTotal;
          }
          if (func.invokeTotal) {
            functionData.invokeCoverage = func.invokeCovered / func.invokeTotal;
          }
          if (func.invokedFuncTotal) {
            functionData.invokedFuncCoverage = func.invokedFuncCovered / func.invokedFuncTotal;
          }
          if (func.failedCount > 0 || func.succeedCount > 0) {
            functionData.failed = func.failedCount > 0;
          }
          if (func.unread === true || (fileElement.canUpdateUnreadStatus(func.kind) && func.unreadCount && func.unreadCount > 0)) {
            functionData.unread = true;
          }
          publisher.publishFunctionMessage('update', functionData);
        });
        return Promise.resolve();
      })
      .then(resolve)
      .catch(reject);
  });
}

const increaseFunctionParentsMerged = increaseActionMerger(increaseFunctionParents);
/**
 * 更新函数增量数据
 * @param {String} functionId 函数id
 * @param {Object} incDoc 增量数据
 * @return {Promise}
 */
async function increaseFunction(functionId, incDoc, {
  batch = false,
  func = undefined,
} = {}) {
  if (!functionId) {
    return;
  }
  functionId = mongoose.Types.ObjectId(functionId);
  func = func || await Func.findOne({ _id: functionId });
  if (!func) {
    return;
  }
  const { fileId } = func;
  if (func.depth && func.depth > 1) {
    await (batch ? increaseFunctionParentsMerged(func.parents, incDoc) : increaseFunctionParents(func.parents, incDoc));
  }
  if (fileId) {
    await (batch ? increaseFileMerged(fileId, incDoc, { batch: true }) : increaseFile(fileId, incDoc));
  }
}

/**
 * Export model definition object.
 */
module.exports = {
  complementByUserId,
  complementByProjectId,
  complementByProjectVersionId,
  complementByFileId,

  renameMongoKey,
  getCountOfFile,

  increaseDirectories,
  increaseFile,
  increaseFunction,
  increaseFunctionParents,
  increaseDirectoriesMerge: increaseDirectoriesMerged,
};
